package com.whz.iplocation.ipregion.search.db;


import com.whz.iplocation.utils.ToolUtils;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteOrder;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 * ip定位查找工具(使用内存映射文件方式读取，线程安全)
 */
public class RCFIpLocation {

	private static final int IP_RECORD_LENGTH = 7;

	private static final byte REDIRECT_MODE_1 = 0x01;

	private static final byte REDIRECT_MODE_2 = 0x02;

	private MappedByteBuffer mbbFile;

	private static Long lastModifyTime = 0L;

	public static boolean enableFileWatch = false;

	private static final ReentrantLock lock = new ReentrantLock();

	private final File queryFile;

	private long firstIndexOffset;

	private long totalIndexCount;

	public RCFIpLocation(String filePath) throws Exception {
		this.queryFile = new File(filePath);
		load();
		if (enableFileWatch) {
			watch();
		}
	}

	private void watch() {
		Executors.newScheduledThreadPool(1).scheduleAtFixedRate(() -> {
			long time = queryFile.lastModified();
			if (time > lastModifyTime) {
				lastModifyTime = time;
				try {
					load();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}, 1000L, 30000L, TimeUnit.MILLISECONDS);
	}

	public long read4ByteAsLong(long pos) {
		mbbFile.position((int) pos);
		return 0xFFFFFFFFL & mbbFile.getInt();
	}

	public long read3ByteAsLong(long pos) {
		mbbFile.position((int) pos);
		return 0xFFFFFFL & mbbFile.getInt();
	}


	@SuppressWarnings("resource")
	private void load() throws Exception {
		lastModifyTime = queryFile.lastModified();
		lock.lock();
		try {
			mbbFile = new RandomAccessFile(queryFile, "r")
					.getChannel()
					.map(FileChannel.MapMode.READ_ONLY, 0, queryFile.length());
			mbbFile.order(ByteOrder.LITTLE_ENDIAN);
			firstIndexOffset = read4ByteAsLong(0);
			long lastIndexOffset = read4ByteAsLong(4);
			totalIndexCount = (lastIndexOffset - firstIndexOffset) / IP_RECORD_LENGTH + 1;
		} finally {
			lock.unlock();
		}
	}

	private long search(long ip) {
		long low = 0;
		long high = totalIndexCount;
		long mid = 0;
		while (low <= high) {
			mid = (low + high) >>> 1;
			long indexIP = read4ByteAsLong(firstIndexOffset + (mid - 1) * IP_RECORD_LENGTH);
			long nextIndexIP = read4ByteAsLong(firstIndexOffset + mid * IP_RECORD_LENGTH);
			if (indexIP <= ip && ip < nextIndexIP) {
				return read3ByteAsLong(firstIndexOffset + (mid - 1) * IP_RECORD_LENGTH + 4);
			} else {
				if (ip > indexIP) {
					low = mid + 1;
				} else if (ip < indexIP) {
					high = mid - 1;
				}
			}
		}
		return -1;
	}

	private Region readIPLocation(long offset, Region loc) {
		try {
			mbbFile.position((int) offset + 4);
			byte redirectMode = mbbFile.get();
			if (redirectMode == REDIRECT_MODE_1) {
				long countryOffset = read3ByteAsLong((int) offset + 5);
				mbbFile.position((int) countryOffset);
				redirectMode = mbbFile.get();
				if (redirectMode == REDIRECT_MODE_2) {
					loc.setCountry(readString(read3ByteAsLong(countryOffset + 1)));
					mbbFile.position((int) countryOffset + 4);
				} else {
					loc.setCountry(readString(countryOffset));
				}
				loc.setArea(readArea(mbbFile.position()));
			} else if (redirectMode == REDIRECT_MODE_2) {
				loc.setCountry(readString(read3ByteAsLong((int) offset + 5)));
				loc.setArea(readArea((int) offset + 8));
			} else {
				loc.setCountry(readString(mbbFile.position() - 1));
				loc.setArea(readArea(mbbFile.position()));
			}
			return loc;
		} catch (Exception e) {
			return null;
		}
	}

	private String readArea(int offset) {
		mbbFile.position(offset);
		byte redirectMode = mbbFile.get();
		if (redirectMode == REDIRECT_MODE_1 || redirectMode == REDIRECT_MODE_2) {
			long areaOffset = read3ByteAsLong((int) offset + 1);
			if (areaOffset == 0) {
				return "";
			} else {
				return readString(areaOffset);
			}
		} else {
			return readString(offset);
		}
	}

	private String readString(long offset) {
		try {
			mbbFile.position((int) offset);
			byte[] buf = new byte[128];

			int i = 0;
			for (buf[i] = mbbFile.get(); buf[i] != 0; ) {
				buf[++i] = mbbFile.get();
			}

			if (i != 0) {
				return new String(buf, 0, i, "GBK");
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		return "";
	}

	public Region fetchIPLocation(String ip) {
		lock.lock();
		try {
			Region location = new Region();
			location.setIp(ip);
			long offset = search(ToolUtils.ipToNum(ip));
			if (offset != -1) {
				return readIPLocation(offset, location);
			}
		} finally {
			lock.unlock();
		}
		return null;
	}
}